﻿using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using CommunityToolkit.Maui.SourceGenerators.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Internal.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Internal.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace CommunityToolkit.Maui.SourceGenerators.Internal;

[Generator]
public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator
{
	static readonly SemanticValues emptySemanticValues = new(default, []);

	const string bpFullName = "global::Microsoft.Maui.Controls.BindableProperty";
	const string bindingModeFullName = "global::Microsoft.Maui.Controls.";

	const string bpAttribute =
		/* language=C#-test */
		//lang=csharp
		$$"""
		// <auto-generated>
		// See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
		
		#pragma warning disable
		#nullable enable
		namespace CommunityToolkit.Maui;

		[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
		[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
		sealed partial class BindablePropertyAttribute : Attribute
		{
			public string? PropertyName { get; }
			public Type? DeclaringType { get; set; }
			public object? DefaultValue { get; set; }
			public string DefaultBindingMode { get; set; } = string.Empty;
			public string ValidateValueMethodName { get; set; } = string.Empty;
			public string PropertyChangedMethodName { get; set; } = string.Empty;
			public string PropertyChangingMethodName { get; set; } = string.Empty;
			public string CoerceValueMethodName { get; set; } = string.Empty;
			public string DefaultValueCreatorMethodName { get; set; } = string.Empty;
		
			public BindablePropertyAttribute(string propertyName)
			{
				PropertyName = propertyName;
			}

			public BindablePropertyAttribute()
			{
			}
		}
		""";

	public void Initialize(IncrementalGeneratorInitializationContext context)
	{

#if DEBUG

		if (!Debugger.IsAttached)
		{
			// To debug this SG, uncomment the line below and rebuild the SourceGenerator project.

			//Debugger.Launch();
		}
#endif

		context.RegisterPostInitializationOutput(static ctx => ctx.AddSource("BindablePropertyAttribute.g.cs", SourceText.From(bpAttribute, Encoding.UTF8)));

		var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.BindablePropertyAttribute",
				SyntaxPredicate, SemanticTransform)
			.Where(static x => x.ClassInformation != default || !x.BindableProperties.IsEmpty)
			.Collect();


		context.RegisterSourceOutput(provider, ExecuteAllValues);
	}

	static void ExecuteAllValues(SourceProductionContext context, ImmutableArray<SemanticValues> semanticValues)
	{
		var groupedValues = semanticValues
		.GroupBy(static sv => (sv.ClassInformation.ClassName, sv.ClassInformation.ContainingNamespace))
		.ToDictionary(static d => d.Key, static d => d.ToArray());

		foreach (var keyValuePair in groupedValues)
		{
			var (className, containingNamespace) = keyValuePair.Key;
			var values = keyValuePair.Value;

			if (values.Length is 0 || string.IsNullOrEmpty(className) || string.IsNullOrEmpty(containingNamespace))
			{
				continue;
			}

			var bindableProperties = values.SelectMany(static x => x.BindableProperties).ToImmutableArray();

			var classAccessibility = values[0].ClassInformation.DeclaredAccessibility;

			var combinedClassInfo = new ClassInformation(className, classAccessibility, containingNamespace);
			var combinedValues = new SemanticValues(combinedClassInfo, bindableProperties);

			var source = GenerateSource(combinedValues);
			SourceStringService.FormatText(ref source);
			context.AddSource($"{className}.g.cs", SourceText.From(source, Encoding.UTF8));
		}
	}


	static string GenerateSource(SemanticValues value)
	{
		var sb = new StringBuilder(
			/* language=C#-test */
			//lang=csharp
			$$"""

			  // <auto-generated>
			  // Test2 : {{DateTime.Now}}

			  namespace {{value.ClassInformation.ContainingNamespace}};

			  {{value.ClassInformation.DeclaredAccessibility}} partial class {{value.ClassInformation.ClassName}}
			  {

			  """);

		foreach (var info in value.BindableProperties)
		{
			GenerateBindableProperty(sb, info);
			GenerateProperty(sb, info);
		}

		sb.AppendLine().Append('}');
		return sb.ToString();

		static void GenerateBindableProperty(StringBuilder sb, BindablePropertyModel info)
		{
			/*
			/// <summary>
			/// Backing BindableProperty for the <see cref="PropertyName"/> property.
			/// </summary>
			*/
			sb.AppendLine("/// <summary>")
				.AppendLine($"/// Backing BindableProperty for the <see cref=\"{info.PropertyName}\"/> property.")
				.AppendLine("/// </summary>");

			// public static readonly BindableProperty TextProperty = BindableProperty.Create(...);
			sb.AppendLine($"public static readonly {bpFullName} {info.PropertyName}Property = ")
				.Append($"{bpFullName}.Create(")
				.Append($"\"{info.PropertyName}\", ")
				.Append($"typeof({info.ReturnType}), ")
				.Append($"typeof({info.DeclaringType}), ")
				.Append($"{info.DefaultValue}, ")
				.Append($"{bindingModeFullName}{info.DefaultBindingMode}, ")
				.Append($"{info.ValidateValueMethodName}, ")
				.Append($"{info.PropertyChangedMethodName}, ")
				.Append($"{info.PropertyChangingMethodName}, ")
				.Append($"{info.CoerceValueMethodName}, ")
				.Append($"{info.DefaultValueCreatorMethodName}")
				.Append(");");

			sb.AppendLine().AppendLine();
		}

		static void GenerateProperty(StringBuilder sb, BindablePropertyModel info)
		{
			// The code below creates the following Property:
			//
			//	public partial string Text
			//	{
			//		get => (string)GetValue(TextProperty);
			//		set => SetValue(TextProperty, value);
			//	}
			//

			sb.AppendLine($"public partial {info.ReturnType} {info.PropertyName}")
				.AppendLine("{")
				.Append("get => (")
				.Append(info.ReturnType)
				.Append(")GetValue(")
				.AppendLine($"{info.PropertyName}Property);")
				.Append("set => SetValue(")
				.AppendLine($"{info.PropertyName}Property, value);")
				.AppendLine("}");
		}
	}

	static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
	{
		var propertyDeclarationSyntax = Unsafe.As<PropertyDeclarationSyntax>(context.TargetNode);
		var semanticModel = context.SemanticModel;
		var propertySymbol = (IPropertySymbol?)semanticModel.GetDeclaredSymbol(propertyDeclarationSyntax, cancellationToken);

		if (propertySymbol is null)
		{
			return emptySemanticValues;
		}

		var @namespace = propertySymbol.ContainingNamespace.ToDisplayString();
		var className = propertySymbol.ContainingType.Name;
		var classAccessibility = propertySymbol.ContainingSymbol.DeclaredAccessibility.ToString().ToLower();
		var returnType = propertySymbol.Type;

		var propertyInfo = new ClassInformation(className, classAccessibility, @namespace);

		var bindablePropertyModels = new List<BindablePropertyModel>(context.Attributes.Length);

		var attributeData = context.Attributes[0];
		bindablePropertyModels.Add(CreateBindablePropertyModel(attributeData, propertySymbol.Type.ToDisplayString(), propertySymbol.Name, returnType));

		return new(propertyInfo, bindablePropertyModels.ToImmutableArray());
	}

	static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in string declaringTypeString, in string defaultName, in ITypeSymbol returnType)
	{
		if (attributeData.AttributeClass is null)
		{
			throw new ArgumentException($"{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData.AttributeClass));
		}

		var defaultValue = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValue));
		var coerceValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName));
		var defaultBindingMode = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultBindingMode), "BindingMode.OneWay");
		var defaultValueCreatorMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName));
		var declaringType = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DeclaringType), declaringTypeString);
		var propertyChangedMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName));
		var propertyChangingMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName));
		var propertyName = attributeData.GetConstructorArgumentsAttributeValueByNameAsString(defaultName);
		var validateValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));

		return new BindablePropertyModel
		{
			CoerceValueMethodName = coerceValueMethodName,
			DefaultBindingMode = defaultBindingMode,
			DefaultValue = defaultValue,
			DefaultValueCreatorMethodName = defaultValueCreatorMethodName,
			DeclaringType = declaringType,
			PropertyChangedMethodName = propertyChangedMethodName,
			PropertyChangingMethodName = propertyChangingMethodName,
			PropertyName = propertyName,
			ReturnType = returnType,
			ValidateValueMethodName = validateValueMethodName
		};
	}

	static bool SyntaxPredicate(SyntaxNode node, CancellationToken cancellationToken) =>
		node is PropertyDeclarationSyntax { AttributeLists.Count: > 0 };
}